package apibasej.bkp;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;

import apibasej.basic.exception.APIBaseException;
import apibasej.basic.misc.UtilCodec;
import apibasej.basic.misc.io.UtilFile;
import apibasej.bkp.model.BACKUP;
import apibasej.bkp.model.FILE_INFO;
import apibasej.bkp.model.INFO_BKPS;
import apibasej.db.server.exec.TransactionServer;
import apibasej.db.sql.wrapper.ColSelect;
import apibasej.db.transaction.CursorSelect;
import apibasej.db.transaction.Row;
import apibasej.db.transaction.SeqManager;
import apibasej.db.transaction.UtilDB;
import apibasej.db.transaction.UtilDBException;

/**
 * http://code.google.com/p/apibasej/
 * 
 * @author Julio Azevedo Lessa - apibasej@gmail.com
 * 
 */
public class UtilBkpDiff extends UtilBkp{

	public static final int SIZE_BUFFER = 100*1024;
	public static final String NAME_FILE_MDB = "INFO_BKPS_FULL.mdb";
	
	
	private Integer idCurrentBackup;
	private Date now;
	private TransactionServer conn;
	
	private String dbUrl;
	private String dbUser;
	private String dbPass;
	
	private MessageDigest md;
	
	
	// * ver opção de zipar a pasta de backup, o log e o mbd após o backup. Fazer separado do UtilBkp? ...
	
	// DICA: depois do backup a pasta gerada e o log podem ser compactados e criptografados, para os próximos backups só precisa do banco ...
	// caso nos pacotes tenham o banco, lembrar que no restore o que vale é o último banco, os dbs dentro dos pacotes são da data do atual backup
	// criar cópia do mdb com data após backup para caso queira voltar ...
	public UtilBkpDiff(String dirRead, String dirWrite) throws NoSuchAlgorithmException{
		super(dirRead, dirWrite);
		//getConfig().loadLocaleAndTimeZone();
		this.md = MessageDigest.getInstance("SHA1");
		this.now = getDateNow();
	}
	

	private File dirWriteBkp;
	public File getDirWriteBkpWithPrefixDate(){
		if(this.dirWriteBkp==null){
			this.dirWriteBkp = new File(getDirWrite()+getPrefixDate());
			this.dirWriteBkp.mkdirs();
		}
		return this.dirWriteBkp;
	}
	

	
	
	
	
	
	/*
	private int getNextIdBackup() throws UtilDBException{
		Integer id = null;
		UtilDB db = new UtilDB(INFO_BKPS.BACKUP);
		ColSelect<Integer> colMaxId = db.addColSelect(BACKUP.ID);
		colMaxId.setStrForColSelect("MAX("+BACKUP.ID.getName()+")");
		Row r = db.getNextResultRow();
		if(r!=null) id = r.get(colMaxId); // retorna null se a tab tiver vazia
		if(id==null) id = 0;
		db.endSelect();// não esquecer se usar o getNextResultRow fora de um while ...
		return id+1;
	}
	*/
	private void insertRowBackup() throws Exception{
		// não chamar funções do UtilDB dentro de outras para não misturar! chamar uma de cada vez 
		idCurrentBackup = SeqManager.getNextSeq(BACKUP.ID, conn);//getNextIdBackup();
		boolean isFirst = idCurrentBackup==1;// revisar!
		
		UtilDB db = new UtilDB(INFO_BKPS.BACKUP);
		db.addEntry(BACKUP.ID, idCurrentBackup);
		db.addEntry(BACKUP.DATE_BKP, new java.sql.Timestamp(now.getTime()) );
		db.addEntry(BACKUP.DIR_READ, getDirRead());
		db.addEntry(BACKUP.DIR_WRITE, getDirWrite());
		db.addEntry(BACKUP.IS_FIRST, isFirst);
		db.addEntry(BACKUP.REGEX, getRegexValidFiles());
		db.execInsert(conn);
		
		printInfo("REGEX: "+getRegexValidFiles());
		printInfo("BACKUP ROW INSERTED: \r\nID="+idCurrentBackup+
				"; \r\nDATE_BKP="+new SimpleDateFormat(getProp(PATTERN_DATE_TIME)).format(now)+
				"; \r\nDIR_READ="+getDirRead()+
				"; \r\nDIR_WRITE="+getDirWrite()+
				"; \r\nIS_FIRST="+isFirst+";");
	}
	
	private Integer lastIdFileInfo = null;// para melhor desempenho na geração dos id do FILE_INFO
	private int getNextIdFileInfo() throws Exception{
		if(lastIdFileInfo==null){
			/*
			UtilDB db = new UtilDB(INFO_BKPS.FILE_INFO);
			ColSelect<Integer> colMaxId = db.addColSelect(FILE_INFO.ID);
			colMaxId.setStrForColSelect("MAX("+FILE_INFO.ID.getName()+")");
			Row r = db.getNextResultRow();
			if(r!=null) lastIdFileInfo = r.get(colMaxId); // retorna null se a tab tiver vazia
			if(lastIdFileInfo==null) lastIdFileInfo = 0;
			db.endSelect();// não esquecer se usar o getNextResultRow fora de um while ...
			*/
			lastIdFileInfo = SeqManager.getNextSeq(FILE_INFO.ID, conn);
		}
		lastIdFileInfo = lastIdFileInfo+1;
		return lastIdFileInfo;
	}

	
	
	// copy and return SHA1
	private String copy(File currentFile, String pathRelative, long lastModified, boolean isDir, String sha1) throws IOException{
		File out = new File(getDirWriteBkpWithPrefixDate(),pathRelative);
		//out.setLastModified(lastModified);// setar depois do close() ... senão não funciona
		out.getParentFile().mkdirs();// para criar as pastas acima, senão da erro ao criar o arquivo 
		if(isDir){
			out.mkdirs();
			return null;
		}else{
			FileInputStream fis = new FileInputStream(currentFile);
			FileOutputStream fos = new FileOutputStream(out);
			byte[] buf = new byte[SIZE_BUFFER];
			int len;
			if(sha1==null) md.reset(); 
			while ( (len = fis.read(buf)) > 0) {
				fos.write(buf, 0, len);
				if(sha1==null) md.update(buf, 0, len);
			}
			if (fis != null) fis.close();
			if (fos != null) fos.close();
			out.setLastModified(currentFile.lastModified());
			//out.setReadOnly(); // besteira ... irá compactar depois
			if(sha1==null) return UtilCodec.encodeHex(md.digest());
			else return sha1;
		}
	}
	// return SHA1
	public String getSha1ToVerify(File f) throws IOException {// sha1 para verificar antes da cópia
		FileInputStream fis = new FileInputStream(f);
        byte[] buffer = new byte[SIZE_BUFFER];
        int len;
        md.reset();
        while ((len = fis.read(buffer)) > 0) {
        	md.update(buffer, 0, len);
        }
        fis.close(); 
        return UtilCodec.encodeHex( md.digest() );
	}
	
	@Override
	public void beforeBackup() throws Exception{
		this.conn = new TransactionServer(getDbUrl(), getDbUser(), getDbPass());
		this.conn.beginTransaction();// só comita no final se for tudo ok
		getDirWriteBkpWithPrefixDate();// somente para criar o diretório
		printInfo("*** INSERTING ROW FOR BACKUP ***");
		printInfo("---------------------------------------------------------------------------");
//		try {
			insertRowBackup();
//		} catch (UtilDBException e1) {
//			printInfo("ERROR ON INSERT ROW IN TABLE BACKUP: "+e1,e1);
//			return;
//		}
		printInfo("---------------------------------------------------------------------------");
	}
	@Override
	public void afterBackup() throws IOException{
		if(this.conn!=null) {
			try {
				this.conn.commitTransaction(true);
			} catch (UtilDBException e) {
				printInfo("ERROR ON CLOSE UtilConnection: "+e.getMessage(),e);
			}
		}
		UtilFile.copyFile(getFileDB(),getFileCopyDB());// cria uma cópia para caso quiera voltar ...
	}
	
	private File fileBkpDB;
	public File getFileDB() {
		if(fileBkpDB==null){
			fileBkpDB = new File(getDirWrite(),NAME_FILE_MDB);
		}
		return fileBkpDB;
	}
	
	private File fileCopyDB;
	public File getFileCopyDB(){
		if(fileCopyDB==null){
			fileCopyDB = new File(getFileDB().getPath().replaceAll(NAME_FILE_MDB,getPrefixDate()+"_"+(NAME_FILE_MDB.replaceAll("_FULL","")) ));
		}
		return fileCopyDB;
	}
	
	// analiza os arquivos antigos bakupeados para o arquivo atual
	// (se tiver tamanho igual e lastModified() igual encarar que o arquivo não foi modificado)
	// (se tiver tamanho diferente significa que foi modificado)
	// (se tiver tamanho igual e lastModified() diferente verificar o hash)
	@Override
	public void verifyFileSource(File currentFile) throws Exception{
		boolean isDir = currentFile.isDirectory();
		long len = currentFile.length();
		this.totalSizeFilesRead += len;
		long lastModified = currentFile.lastModified();
		String pathRelative = currentFile.getPath().substring(getDirRead().length());
		
		// ****** verifica se tem arquivo exatamente igual em backups anteriores que não foi removido, se tiver, ignora este arquivo
		UtilDB db = new UtilDB(INFO_BKPS.FILE_INFO);
		ColSelect<Integer> cId = db.addColSelect(FILE_INFO.ID);// para não trazer todas colunas
		db.addColSelect(FILE_INFO.IS_REMOVED);// para não trazer todas colunas
		db.getWhere().addFilter(FILE_INFO.IS_DIR, isDir);
		db.getWhere().addFilter(FILE_INFO.PATH, pathRelative);
		db.addColOrderBy(cId).setIsDesc(true);// decrescente para se tiver algum removed depois do outro idêntico
		if(!isDir){// se não for dir não verificar length e lastModified
			db.getWhere().addFilter(FILE_INFO.LENGTH,len);
			db.getWhere().addFilter(FILE_INFO.LAST_MODIFIED, lastModified );
		}
		// se já tiver um link criado antes o link será encontrado e visto como idêntico, e irá cair no return ..
		for(Row r : db.execSelect(conn)){
			// se tiver algum removed idêntico depois do outro idêntico este arquivo passa para verificação depois
			// não deve ser encarado como igual pois foi deletado no ultimo backup, verificar depois os links ...
			// este caso só irá ocorrer se remover um arquivo em um backup e depois voltar exatamente igual
			if(r.get(FILE_INFO.IS_REMOVED)) break;
			
			return;// se chegar aqui ignorar este arquivo, pois é idêntico e não foi removido 
		}
		// ******************************************************
		
		//*===> só chega aqui se o arquivo atual não está exatamente igual em um último backup
		
		Row oldEqual = null;
		String sha1CurrentFile = null;
		
		// *********** verifica se tem arquivo igual salvo antes para setar referencia invés de copiar *************
		if(!isDir && len>0){// se tiver arquivos vazios não setar referência ... copiar é melhor ...
			db = new UtilDB(INFO_BKPS.FILE_INFO);
			ColSelect<Integer> cId2 = db.addColSelect(FILE_INFO.ID);
			db.addColSelect(FILE_INFO.SHA1);
			db.getWhere().addFilter(FILE_INFO.IS_REMOVED, false);// não verificar linhas que indicam remoção de arquivos
			db.getWhere().addFilter(FILE_INFO.IS_DIR, false);// se tinha dir igual já entrou na verificação anterior
			db.getWhere().addFilter(FILE_INFO.LENGTH, len);// varre todos com length igual para procurar algum com hash igual
			db.getWhere().addFilter(FILE_INFO.ID_FILE_REFERENCE, null).setOperatorForValue("IS NULL"); // para verificar links somente para arquivos que foram copiados (existam fisicamente) e não links para outros links
			db.addColOrderBy(cId2);// crescente para conferir sempre com o primeiro salvo, para todos iguais apontarem sempre para o mesmo
			CursorSelect cs = db.createCursorSelect(conn);
			Row rOld;
			while( (rOld=db.getNextResultRow(cs))!=null ){
				if(sha1CurrentFile==null) sha1CurrentFile = getSha1ToVerify(currentFile);// so verifica sha1 se tiver arquivos com mesmo tamanho
				if(sha1CurrentFile.equalsIgnoreCase(rOld.get(FILE_INFO.SHA1))){
					oldEqual = rOld;
					break;// se achar não precisa ficar vendo outros
				}
			}
			cs.verifyCloseConnectionAfterExecute(); // precisa pois tem break dentro do while ...
		}
		// ******************************************************
		
		// se não tem antigo igual copia o arquivo e retorna o sha1, pastas retornam sha1 null
		// se tiver igual (oldEqual!=null) o sha1CurrentFile já estará setado
		// se tiver setado o sha1 antes informa ao copy(...) para não precisar gerar denovo
		if(oldEqual==null) {
			totalSizeFilesWrite += len;
			sha1CurrentFile = copy(currentFile,pathRelative,lastModified,isDir,sha1CurrentFile);
		}
		
		// INSERIR LINHA ...
		insertFileInfo(pathRelative, 
				isDir, 
				false, 
				len, 
				lastModified, 
				sha1CurrentFile, 
				idCurrentBackup, 
				oldEqual!=null?oldEqual.get(FILE_INFO.ID):null); // link para arquivo igual salvo antes, se tiver não copia
		
	}


	
	// verifica se teve algum arquivo removido nos atuais baseado na estrutura do que seria o último restore (última estrutura de arquivos)
	@Override
	public void verifyRemoveds() throws Exception{
		UtilDB dbAux = new UtilDB(INFO_BKPS.FILE_INFO);
		dbAux.addColSelect(FILE_INFO.ID);
		dbAux.addColSelect(FILE_INFO.PATH);
		dbAux.addColSelect(FILE_INFO.IS_DIR);
		dbAux.addColSelect(FILE_INFO.LENGTH);
		dbAux.addColSelect(FILE_INFO.LAST_MODIFIED);
		dbAux.addColSelect(FILE_INFO.SHA1);
		dbAux.getWhere().addFilter(FILE_INFO.ID_BACKUP, idCurrentBackup).setOperatorForValue("<");// não precisa ver os incluidos do backup atual
		dbAux.getWhere().addFilter(FILE_INFO.IS_REMOVED,false);
		TransactionServer c2 = new TransactionServer(getDbUrl(), getDbUser(), getDbPass());
		CursorSelect cs = dbAux.createCursorSelect(c2); //// usa nova conn porque faz insert dentro do loop usando o this.db ???
		Row r;
		while( (r=dbAux.getNextResultRow(cs))!=null ){// não precisa do verifyCloseAfterExecute() porque irá chamar o close quando o getNextResultRow retornar null
			String pathOldFile = r.get(FILE_INFO.PATH);
			// se não existe nos atuais
			if( !new File(getDirRead(),pathOldFile).exists() ){
				
				UtilDB db = new UtilDB(INFO_BKPS.FILE_INFO);
				//ColSelect<Long> count = this.db.addColSelectAsString("COUNT(*)",null,Long.class);
				db.addColSelect(FILE_INFO.ID);
				
				// se tem antigo com path igual depois, independente se é removido ou não, indica que este path não faz parte do último backup
				db.getWhere().addFilter(FILE_INFO.ID_BACKUP, idCurrentBackup).setOperatorForValue("<");// não precisa ver os incluidos do backup atual
				db.getWhere().addFilter(FILE_INFO.ID, r.get(FILE_INFO.ID) ).setOperatorForValue(">");// para verificar se tem path igual após a linha atual
				db.getWhere().addFilter(FILE_INFO.PATH, pathOldFile);
				
				CursorSelect cs2 = db.createCursorSelect(conn);
				if(db.getNextResultRow(cs2)==null){// se não tem 
					// setar todos atributos do arquivo removido neste registro para identificar depois no verifyOlds ...
					insertFileInfo(r.get(FILE_INFO.PATH), 
							r.get(FILE_INFO.IS_DIR), 
							true, 
							r.get(FILE_INFO.LENGTH), 
							r.get(FILE_INFO.LAST_MODIFIED), 
							r.get(FILE_INFO.SHA1), 
							idCurrentBackup, 
							r.get(FILE_INFO.ID) );// id do arquivo que foi removido 
				}
				cs2.verifyCloseConnectionAfterExecute();//precisa, pois usa só o getNextResultRow somente uma vez
			}
		}
		cs.verifyCloseConnectionAfterExecute();//somente por segurança //não precisa do verifyCloseAfterExecute() porque irá chamar o close quando o getNextResultRow retornar null
		//dbAux.closeThisTransaction();
	}
	
	private void insertFileInfo(String path, boolean isDir, boolean isRemoved, long length, long lastModified, String sha1, int idBackup, Integer idFileReference) throws Exception{
		int idFileInfo = getNextIdFileInfo();// não chamar funções do UtilDB dentro de outras para não misturar! chamar uma de cada vez 
		UtilDB db = new UtilDB(INFO_BKPS.FILE_INFO);
		db.addEntry(FILE_INFO.ID, idFileInfo);
		db.addEntry(FILE_INFO.ID_BACKUP, idBackup);
		db.addEntry(FILE_INFO.ID_FILE_REFERENCE, idFileReference);
		db.addEntry(FILE_INFO.IS_DIR, isDir);
		db.addEntry(FILE_INFO.IS_REMOVED, isRemoved);
		db.addEntry(FILE_INFO.LAST_MODIFIED, lastModified);
		db.addEntry(FILE_INFO.LENGTH, length);
		db.addEntry(FILE_INFO.PATH, path);
		db.addEntry(FILE_INFO.SHA1, sha1);
		db.execInsert(conn);
		
		if(isRemoved) {
			ctRemoveds++;
			printInfo("[REMOVED :"+idFileInfo+"]\t "+path+"; isDir="+isDir);
		}else if(isDir) {
			ctDirsCreateds++;
			printInfo("[NEW_DIR :"+idFileInfo+"]\t "+path);
		}else if(idFileReference!=null) {
			ctReferences++;
			printInfo("[NEW_LINK:"+idFileInfo+"]\t "+path+"; [ID_REFERENCE:"+idFileReference+"]");
		}else{
			ctFilesCopieds++;
			printInfo("[NEW_FILE:"+idFileInfo+"]\t "+path);
		}
	}

	

	public String getDbUrl() throws APIBaseException {
		if(dbUrl==null){
			File fileBkpDB = new File(getDirWrite(),NAME_FILE_MDB);
			if(!fileBkpDB.exists()) throw new APIBaseException("DEFAULT DATABASE FILE NOT FOUND: "+fileBkpDB.getPath());
			return "jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ="+fileBkpDB.getPath()+";}";// por padrão procura no raiz da partição atual ... recomendado setar a URL !!!;
		}
		return dbUrl;
	}
	public void setDbUrl(String dbUrl) {
		this.dbUrl = dbUrl;
	}
	public String getDbUser() {
		return dbUser;
	}
	public void setDbUser(String dbUser) {
		this.dbUser = dbUser;
	}
	public String getDbPass() {
		return dbPass;
	}
	public void setDbPass(String dbPass) {
		this.dbPass = dbPass;
	}
	public Integer getIdCurrentBackup() {
		return idCurrentBackup;
	}

	
	
}
